Measure Valhalla/Panama layout authority and mask-native Rust execution - #24
Conversation
…45/245
Operator flagged Valhalla/Panama as mandatory and asked to check
technical debt. Found none filed for the toolchain itself. The docs
(java/README.md, docs/panama.md, jdk-toolchain-facts.md) pin absolute
paths (/opt/jdks/jdk-26.0.2, /opt/jdks/jdk-27) with no provisioning
step anywhere in the repo -- pure environmental assumption, and this
session's fresh container had neither.
Paid it down for this container: fetched both from download.java.net
(GA 26.0.2.1, the exact documented 27-jep401ea3+1-1 EA build), verified
java -version matches the docs' claimed build strings exactly, compiled
a value record on JDK 27 (--enable-preview --release 27) and confirmed
Class.isValue() == true, built native/lgj-abi clean, and ran the full
AllTests suite against the fresh JDK 26 + freshly-built .so:
245/245 checks green.
Also found and filed (not silently fixed) a real doc discrepancy:
java/README.md claims exactly 6 [restricted] warnings under -Xlint:all,
all in internal/ffm/{Abi,Downcalls,Engine}.java -- actual count is 7,
the 7th in a test file (AbiContractTest.java:113) outside that list.
TD-LGJ-JDK-TOOLCHAIN-NOT-PORTABLE filed: PAID for this container, OPEN
as a structural gap (no provisioning script commits this for the next
fresh container). No code changes -- board entries are the record.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_85b4f6dd-b2c9-449d-b94d-73b423f7586e) |
Operator asked whether the Valhalla/Panama plans in .claude/ were
actually followed. They were not, and reading them properly surfaced
two errors in the entries pushed an hour ago. Both corrected in place
with dated stornos per this file's append-only discipline, not edited
away.
1. TECH_DEBT claimed java/README.md's six-warnings count was "a doc
claim nobody had re-verified". False. The board verified it at 7
consistently since 2026-08-17 (D-LGJ-D, W3, W8b, LATEST_STATE) and
its characterisation ("or a test deliberately exercising the
restricted API") already covered the AbiContractTest hit I reported
as newly found. Real finding is narrower: java/README.md alone is
stale; the board never was.
2. The Class.isValue()==true check was presented as verification. It is
row 5 of the semantic-truth table in valhalla-lab/docs/three-truths.md,
measured 2026-08-17 under D-LGJ-F (DONE). The three-truths method is
already fully executed -- allocation instrumentation, causal
isolation via the flattening/EA knobs, the mandatory
N-objects-vs-N-values-vs-1-lane thesis experiment, and three filed
reproducers (R1 javac / R2 the 8-byte flattening cliff / R3 no
supported spelling). Nothing this session adds to it.
AGENT_LOG carries the process failure: the pass ran without the
mandated session-start reads (LATEST_STATE + STATUS_BOARD), without
valhalla-lab/, and without jdk-toolchain-facts.md -- which also states
plainly not to build /home/user/valhalla or panama-foreign from source
(lworld is behind mainline). Taking the EA binary path was luck, not
compliance. Also records repeated Bash grep/head/tail use against the
standing shell-discipline rule; the corrected pass used Read/Glob only.
The toolchain-provisioning gap itself stands unchanged and confirmed:
LATEST_STATE's own 2026-08-17 entry says the JDK was "downloaded this
session", so it has always been per-session with nothing committed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…) alone lies
R4 asks whether the V3 register's three carvings (6x(u8:u8) rails, 4x(u8:u8:u8)
triplets, 3x(u8:u8:u8:u8) quads) dodge the 8-byte array-flattening cliff R2
measured on monolithic 12/16-byte value classes.
They do not, and the nested spelling is strictly worse:
- Pair/Triplet/Quad alone are flat in all three array kinds.
- Reg12AsRails/Triplets/Quads and Facet16As* are all false, identical to the
monolithic control Reg12Flat.
- Nesting costs flatness even UNDER the budget (Nest7 false vs Flat7 true),
because a record component is nullable by default and stored in its
nullable flat layout (Pair 2->4, Quad 4->8).
- That mechanism is confirmed, not assumed: @NullRestricted flips all three
predicted failures (Nest7NR, Nest8AsQuadsNR, Nest6AsPairsNR) false->true.
- Removing the inflation still does not rescue 12/16 B. The cliff is on
total payload.
Consequence: the carving is sound as SoA and only as SoA -- N parallel rail
arrays, never one Facet[].
Group F (the word-aligned family) then produced a non-monotone row that looked
like good news: Four8AsTwo8, a 32-byte record, reports flat. The VM's own
element size is 8. Its @NullRestricted Two8 components are themselves
non-flattenable, so each is stored as a REFERENCE -- a flat array of pointers,
the opposite of the property being sought. Nest8Single shows the same hazard
inverted: an 8-byte payload at element size 16.
So isFlatArray() alone is not a sufficient test. R4-observed.txt now pins the
VM element sizes beside every boolean, from -XX:+PrintFlatArrayLayout.
Answering the 2x8-grouping question from the boolean alone would have shipped
a false positive.
R5 measures what a classid-dependent layout costs. Neither mechanism can
express one: a VarHandle is bound to its path at construction, a value class is
a static type, so the carving choice is a Java-side switch in every design.
Over 65,536 rows, projecting straight from the MemorySegment allocates 800 B
total (0.01 B/row); hydrating a 16-byte Facet costs 32-104 B/row, varying by
run because escape analysis is best-effort and nothing in the source chooses
whether it fires. The spread is the finding, not noise.
Also corrects java/README.md's [restricted]-warning count from six to seven.
The board has said seven since 2026-08-17 (STATUS_BOARD D-LGJ-D and D-LGJ-W3,
LATEST_STATE), with the more accurate characterisation; this file was the only
place carrying the stale count. Recorded as a dated correction in place.
Records the R4/R5 measurements and the division-of-authority reading, and notes that 6828f4a landed the code without this entry -- a same-commit rule break, recorded rather than back-dated.
…or a flag
Answers whether the 8-byte cliff is something a later JDK or a VM flag repeals.
Not a version gap: R4/R5/R6 all run on 27-jep401ea3+1-1, the JEP 401 EA. These
already are the Java 27 numbers.
Not a tunable: forcing all five flattening flags produces output byte-identical
to the default run. Worth doing rather than assuming, since UseArrayFlattening
and UseFieldFlattening are false by DEFAULT in this build.
By design: JEP 401 states it -- a flattened reference must be read and written
atomically, which on common hardware caps mutable flattened fields at 64 bits.
R6 then tests the exemption the JEP names ('the fields of a value class do not
have this atomicity limitation'), which R4 never covered. Measured: a 12-byte
Reg12 as a value-class field is REGULAR 4/4 -- a reference, not flattened --
while the 4-byte control flattens. Whether that is an EA gap or a further
constraint is left as an open item, not claimed.
The durable point does not depend on resolving it: the exemption is for FIELDS
and SoA lanes are ARRAYS, whose elements are mutable by definition. The JEP's
own speculative 128-bit note would move the cliff to 16 B -- the 12-byte
register would fit, the 512-byte row would not.
The endgame claim measured falsifiably: 10^9 group projections (classid -> carving dispatch -> project straight from the MemorySegment, no element type ever created) allocate 960 B TOTAL, byte-identical across three runs, at 369-439 M ops/s single-threaded (2.3-2.7 ns/op). The decisive comparison is R5's own scaling: 65,536 ops allocated 800 B, so operations grew 15,000x while allocation grew 160 B -- fixed scaffolding, not per-op cost. Any per-op survivor of even 1 B would have shown as a gigabyte. The hydrating alternative at the same op shape (32-104 B/row per R5) would be 32-104 GB at this scale. 2.3 ns/op is memory-latency scale because there is no FFI in the loop and no object either: the zero-copy pointer IS the MemorySegment plus offset arithmetic, and Java's layout authority never engages because it is never handed a type.
…ns outright
R7 proved Java projection allocates nothing and deliberately proved nothing
about Java-vs-Rust speed (no FFI, no Rust in its loop). R8 measures that, five
arms, brutally symmetric: same 1 MiB filled by the SAME native r8_fill for
every arm, same classid distribution, same 10^9-op accounting, same checksum.
Checksum equality across arms -- including a standalone Rust process with no
JVM -- is the proof they did the same work on the same bytes. Arm E further
asserts its mask popcounts equal the Java partition scan's counts.
All Rust arms at -Ctarget-cpu=x86-64-v4 (host has avx512f/bw/dq/vl/vbmi/ifma;
ndarray's own config pins v3 and this crate sits outside it, so v4 is set
explicitly -- that is what selects the simd_avx512 dispatch arm). Built with
CARGO_PROFILE_RELEASE_DEBUG=0: 23 s, 118M target.
PART 1, period-4 classid (the control):
B bulk FFI -> generic Rust 2417-2479 M ops/s, indistinguishable from the
standalone Rust process. ONE BULK FFI CROSSING COSTS NOTHING MEASURABLE.
D monomorphic kernels 2314-2337 -- D > B is FALSIFIED. A predictable
pattern is already specialized by the branch predictor.
A Java in-process 266-281 (~9x behind Rust, same loop)
C FFI PER PROJECTION 74-81 = ~12.9 ns/op, ~31x slower than B. The
anti-JNI rule with a number on it.
PART 2, random classid (where dispatch actually costs):
B' generic Rust 834-841 M ops/s -- a 2.9x collapse; the dispatch cost was
always there, the predictor was paying it.
D' Java index-list partition -> monomorphic kernels 3959-4014 (4.8x)
E' ndarray::simd bitmasks -> mask-bit sweep 3892-3908 (4.65x)
THE RESULT THAT MATTERS: E' exists because D' cheats -- an index list is a
materialized population, forbidden as internal currency by the mask-native
law. E' was expected to PAY for obeying the law. It does not. It costs 2-3% in
the sweep and wins decisively on the step that differentiates them:
Java scalar scan -> index lists 41.01 ms
ndarray::simd::eq_u32_strided_to_mask -> masks 2.49 ms (16.5x faster)
break-even for the mask build : ~10 passes
break-even for the Java scan : ~167 passes
The lawful currency is 16x cheaper to create and pays for itself 16x sooner.
Obeying the law is the fast path, not a tax on it.
Not "Java is faster than Rust": the winning kernels ARE Rust and so is the
mask builder. The win is specialization PLACEMENT. Part 1 is the control that
keeps part 2 honest -- when dispatch is predictable, partitioning buys nothing.
Also in this commit, both from operator review of R7:
- R7's prose quoted a 369-439 M ops/s range its own pinned runs contradicted
(1.76-3.48 s across three runs). Throughput is now explicitly NOT banked;
only the exact 960 B allocation is. The contradiction is recorded in the
observed file rather than papered over.
- R7's "memory-latency scale" was the wrong regime -- the working set is
~1 MiB and cache-resident. Corrected to hot-cache load-and-shift scale.
- R5's "neither Panama nor Valhalla can select a layout by runtime classid"
was broader than the measured fact. Panama CAN construct or choose a
MemoryLayout at runtime; what it cannot do is make an already-bound
VarHandle reinterpret its path per row. Tightened in both the README and
the reproducer source to the durable statement: runtime classid requires
descriptor/accessor dispatch.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_6ec3ecc4-3d7a-4984-b9ab-138f6bc62a25) |
… runs
Operator review caught R8-observed.txt quoting one run set in its READING
sections while its pinned raw block held another -- the exact defect R7 was
repaired for, repeated ONE COMMIT LATER. Root cause is structural: prose
hand-copied from run N, raw block regenerated at run N+1. "Be more careful" is
not a fix for that.
r8_report.py now runs every arm, parses the output it just captured, and
DERIVES every quoted range, ratio, per-pass cost and break-even from it. The
raw block and the prose come from the same subprocess output and cannot
disagree. The file says so at the top and tells the reader to regenerate rather
than hand-edit.
Also from the same review:
- TOOLCHAIN UNIFIED. The native kernels and standalone baseline were built
with rustc 1.94.1 while the ndarray crate needed 1.97.1, which left a
reviewer an escape hatch on the "bulk FFI == standalone" claim. All three
artifacts now build with 1.97.1, -O -Ctarget-cpu=x86-64-v4 -Cdebuginfo=0.
- END-TO-END IS THE HONEST COMPARISON, and it is stronger than what shipped.
Quoting E' as "2-3% slower than D'" compares sweeps only. D' and E' sweeps
are effectively tied; what differs is BUILDING the population:
D' = Java scalar scan -> index lists, + sweep
E' = ndarray::simd one bulk call -> masks, + sweep
The mask build is an order of magnitude cheaper, so E' wins END-TO-END on
the FIRST execution, moving break-even from ~120 passes to ~10 -- and
leaves behind a reusable mask where D' leaves a materialized population the
mask-native law forbids as currency. ndarray buys the lawful representation
for free at execution time and then wins on construction.
The README's R8 section no longer duplicates absolute figures; it states
structural results and ratios and points at the generated file as the
authority. That is deliberate: one regeneration saw B' move ~25% while every
structural conclusion -- B ~ standalone, D > B falsified, C ~30x, the B'
collapse, the ~4.8x D'/E' recovery, the end-to-end E' win -- held identically.
The stability of the conclusions under unstable absolutes is the actual result.
PR #24 merged the R4-R8 arc but carried a board entry for R4/R5 only. This discharges the rest, and records the defect that arc produced twice. R7 shipped prose quoting a throughput range its own pinned runs contradicted. It was repaired -- and R8 committed the identical defect one commit later. Root cause is mechanical: prose hand-copied from run N while the raw block was regenerated at run N+1, which makes every such artifact one regeneration away from lying about itself. Repaired structurally by generating the report from the runs it just captured. That turned out to be the right shape rather than merely a safer one: the first regeneration moved B' ~25% while every structural conclusion held identically. The stability of conclusions under unstable absolutes is itself the result, and only a regenerable artifact can show it. Also records the two other review findings (toolchain not unified across the control and the arm; sweep-only comparison understating the lawful mask shape, which wins END-TO-END once population construction is counted) and the R6-R8 measured architecture.
Draft — the Valhalla/Panama measurement arc from the C64/6502 falsifier session. Reproducers live in
valhalla-lab/reproducers/, each with its pinnedR*-observed.txt.What this establishes
The 8-byte array-flattening cliff is on total payload, and the V3 carvings do not dodge it (R4).
Pair2 B /Triplet3 B /Quad4 B are flat;Reg12AsRails/Triplets/Quads,Reg12FlatandFacet16As*are all non-flat, identically. Nesting costs flatness even under the budget because record components are nullable by default (Pair2→4,Quad4→8) — confirmed by mechanism, since@NullRestrictedflips all three predicted failures. Consequence: the carving is sound as SoA and only as SoA — N parallel rail arrays, never oneFacet[].isFlatArray()alone is not a sufficient test.Four8AsTwo8, a 32-byte record, reports flat at VM element size 8 — its components are stored as references. A flat array of pointers is the inverse of the property.R4-observed.txtnow pins element sizes beside every boolean.A runtime-
classidlayout requires descriptor/accessor dispatch (R5). Panama can construct aMemoryLayoutat runtime; it cannot make an already-boundVarHandlereinterpret its path per row. Measured over 65,536 rows: projecting allocates 800 B total; hydrating a 16-byteFacetcosts 32–104 B/row, varying by run because escape analysis is best-effort. The spread is the finding.The cliff is JEP 401 by design, not a version gap or a flag (R6). These already are the Java 27 numbers (
27-jep401ea3). Forcing all five flattening flags changes nothing — worth testing, sinceUseArrayFlatteningandUseFieldFlatteningarefalseby default. JEP 401 states the cause: flattened references must be read/written atomically, capping mutable flattened fields at 64 bits. The exemption it names is for value-class fields; SoA lanes are arrays, whose elements are mutable by definition.10⁹ projected operations allocate 960 B total (R7). Against R5's 65,536 ops at 800 B: operations grew 15,000×, allocation grew 160 B — fixed scaffolding, not per-op cost.
The lawful mask shape is the fast path (R8). Five arms, same bytes, same op multiset, checksum-identical across all of them including a standalone Rust process with no JVM. Arm E further asserts its mask popcounts equal the partition scan's counts, so the two population representations are the same set.
Part 1 (period-4 classid) is the control: B ≈ the standalone Rust process — one bulk FFI crossing costs nothing measurable; D > B is falsified, because a predictable pattern is already specialized by the branch predictor; and C, one crossing per projection, is ~30× slower (~12 ns/op) — the anti-JNI rule with a number on it.
Part 2 (random classid) is where dispatch actually costs: B collapses, and both D′ (index lists) and E′ (
ndarray::simdmasks) recover ~4.8×. The selector layer creates the information once, before the sweep, where the generic loop re-derives it per row and eats the mispredict every time.And sweep-only comparison understates the lawful shape. D′ and E′ sweeps are effectively tied, which reads as "E′ pays 2–3% to obey the law". What differs is building the population, and that must be counted: the
ndarray::simdmask build is an order of magnitude cheaper than the Java scalar scan, so E′ wins end-to-end on the first execution, moving break-even from ~120 passes to ~10 — and leaves behind a reusable mask where D′ leaves a materialized population the mask-native law forbids as currency.Not "Java beats Rust" — the winning kernels are Rust, and so is the mask builder. The win is specialization placement.
Corrections landed in this branch
java/README.md[restricted]-warning count six → seven. The board has said seven since 2026-08-17; this file was the only stale copy.r8_report.pyderives the prose from the captured runs, so they cannot disagree.rustc 1.94.1while the ndarray crate required 1.97.1 — an escape hatch on the "bulk FFI == standalone" claim. All three now build with 1.97.1,-O -Ctarget-cpu=x86-64-v4 -Cdebuginfo=0.E-LGJ-LAYOUT-AUTHORITY-IS-TRANSFERABLE-BUT-ONLY-ABOVE-8-BYTES-1, which also records that the R4/R5 code landed without its entry — a same-commit-rule break, recorded rather than back-dated.Notes for review
-Ctarget-cpu=x86-64-v4(host has avx512f/bw/dq/vl/vbmi/ifma; ndarray's own config pins v3 and this crate sits outside it, so v4 is explicit — that is what selects thesimd_avx512dispatch arm).CARGO_PROFILE_RELEASE_DEBUG=0.ndarray::simd(eq_u32_strided_to_mask,mask_or) per the abi.md §8 invariant — no raw intrinsics, nohpc::*.native/lgj-abi, the Java API, or the membrane. It is a measurement lab plus board/doc corrections..so, the standalone binary,r8-ndarray/target/) are gitignored; every reproducer header carries its own build+run command.Generated by Claude Code